################################################################################
#  Copyright (c) by Medtronic, Inc., 2013.  
#  $HeadURL: http://build-tool.googlecode.com/svn/trunk/BuildTool/hextools.pm $
#  $Revision: 71 $
#  $Date: 2013-08-12 13:43:22 +0000 (Mon, 12 Aug 2013) $
################################################################################

################################################################################
#
# This module contains a collection of subroutines that are used to process
# Intel hex files as part of the firmware build process. 
#
#
# An Intel HEX file is an ASCII file with one record per line. Each line  has the   
# following format:                                                                    
#                                                                                      
#   Char 1     Record Marker: The first character of the line is always a colon (ASCII 
#                             0x3A) to identify the line as an Intel HEX file.         
#                                                                                      
#   Char 2-3   Record Length: This field contains the number of data bytes in the      
#                             record represented as a 2-digit hex number. This is the  
#                             total number of data bytes, not including the checksum   
#                             byte nor the first 9 characters of the line.             
#                                                                                      
#   Char 4-7   Address:       This field contains the address where the data should be 
#                             loaded into device memory. This is a value from 0 to     
#                             65,535 represented as a 4-digit hex value.               
#                                                                                      
#   Char 8-9   Record Type:   This field indicates the type of record for this line.   
#                             The possible values are:                                 
#                                00 == normal data                                     
#                                01 == End of file                                     
#                                02 == Extended Segment Address. The data value from   
#                                      this line (chars 10-13), shifted left by 4,     
#                                      will be added to sebsequent addresses. This     
#                                      supports an address range of up to 1MB.         
#                                      This record won't be used by Galaxy, since the  
#                                      CM3 has a 4GB address range.                    
#                                03 == Start address for certain Intel processors.     
#                                      Not used for ARM processors. This record may be 
#                                      generated by tools such as ARM's fromelf, but   
#                                      will be removed by this script.                 
#                                04 == Linear Base Address. The data value from this   
#                                      line (chars 10-13), shifted left by 16, will    
#                                      be added to sebsequent addresses. This supports 
#                                      an address range of up to 4GB.                  
#                                05 == Start address for certain Intel processors.     
#                                      Not used for ARM processors. This record may be 
#                                      generated by tools such as ARM's fromelf, but   
#                                      will be removed by this script.                 
#                                                                                      
#   Char 10-?? Data Bytes:    The following bytes are the actual data that will be     
#                             programmed into device memory. The data is represented   
#                             as 2-digit hex values.                                   
#                                                                                      
#              Checksum:      The last two characters of the line are a checksum for   
#                             the line. The checksum value is calculated by taking     
#                             the two's compliment of the sum of all the preceeding    
#                             data bytes, excluding the checksum byte itself and the   
#                             colon at the beginning of the line.                      
#                                                                                      
# These are examples of normal data records, the first with 16 bytes of data starting  
# at address 0x0030, and the second with 32 bytes of data starting at 0x02A0 (note     
# that the addresses are relative to the most recent Linear Base Address):             
#                                                                                      
#    :10003000119B03104B9B0310859B0310BF9B031058                                       
#    :2002A0007ED32D01010101017ED32D01010101017ED32D01010101017ED32D010101010132       
#                                                                                      
# This is an example of an End-of-File record (note that the address field for this    
# record is always 0000):                                                              
#                                                                                      
#    :00000001FF                                                                       
#                                                                                      
# This is an example of a Linear Base Address record, specifying that 0x10000000       
# should be added to subsequent addresses (note that the address field for this
# record is always 0000):                                                                     
#                                                                                      
#    :020000041000EA                                                                   
#                                                                                      
################################################################################


package hextools;
require Exporter;
@ISA = qw(Exporter);
@EXPORT_OK = qw (
                   Calculate_CRC
                   Calculate_ECC32
                   Checksum
                   Generate_Hex_File
                   Get_Timestamp
                   Init_Log
                   Log
                   Parse_Intel_Hex_File
                );

# Hard-coded Configuration data
my $verbose_file   = 1;  # Output log info to the log file if true
my $verbose_screen = 0;  # Output log info to the screen if true
my $DATA_LENGTH    = 16; # 16 data bytes per hex file line (legacy hex files had
                         # 32 data bytes per line)

1; # end module with an expression that evaluates to true


#****************************************************************************************
#
# Unit Name:    Init_Log
#
# Parameters:   $logfile_name
#
# Return Value: None
#
# Description:  Opens the file with the given file name. Exits on failure to open the file.
#               This allows the hex tools to log error, warnings, etc.
#
#*****************************************************************************************
sub Init_Log
{
   my ($logfile_name) = @_;

   if ($logfile_name) {
       open(LOGFILE, '>', $logfile_name) or die "Can't open $logfile_name: $!\n";
   }
}


#****************************************************************************************
#
# Unit Name:    Log
#
# Parameters:   $message
#
# Return Value: None
#
# Description:  Prints the given message to the log file (if the logfile argument was
#               specified, the log file was successfully opened, and the verbose_file
#               flag is true) and/or to the screen (if the verbose_screen flag is true).
#
#*****************************************************************************************
sub Log
{
   my ($message) = @_;

   if ($verbose_file) {
      printf LOGFILE $message;
   }

   if ($verbose_screen) {
      printf $message;
   }
}


#****************************************************************************************
#
# Unit Name:    Get_Timestamp
#
# Parameters:   None
#
# Return Value: A string containing the current timestamp
#
# Description:  Get the current date and time and convert it to a text string containing
#               month, day, year, hours, minutes.
#
#*****************************************************************************************
sub Get_Timestamp
{
   my ($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst) = localtime(time);
   my $month = ("January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December")[$mon];
   return (sprintf "$month %02d, %04d  %02d:%02d", $mday, $year+1900, $hour, $min);
}


#****************************************************************************************
#
# Unit Name:    Calculate_ECC32
#
# Parameters:   $word: a 4-byte value
#
# Return Value: ECC value for the given word
#
# Description:  Calculates the ECC value for a 4-byte flash word. This is used when
#               generating an RTL file that will be loaded in the simulation environment
#               by "stuffing" the data (and ECC values) into the flash memory (this avoids
#               having to use normal flash programming to load the code and ECC values).
#
#*****************************************************************************************
sub Calculate_ECC32
{
    my ($word) = @_;

    # Get the bits from the value.
    my @b;
    for (my $bit = 0; $bit < 32; $bit++)
    {
        $b[$bit] = ($word >> $bit) & 1;
    }

    my @p;
    $p[0] = $b[0]  ^ $b[1]  ^ $b[3]  ^ $b[4]  ^ $b[6]  ^ $b[8]  ^ $b[9]  ^ $b[11] ^ $b[12] ^ $b[14] ^ $b[16] ^ $b[17] ^ $b[19] ^ $b[20] ^ $b[22] ^ $b[24] ^ $b[25] ^ $b[27] ^ $b[28] ^ $b[30]; 
    $p[1] = $b[0]  ^ $b[2]  ^ $b[3]  ^ $b[5]  ^ $b[6]  ^ $b[8]  ^ $b[10] ^ $b[11] ^ $b[13] ^ $b[14] ^ $b[16] ^ $b[18] ^ $b[19] ^ $b[21] ^ $b[22] ^ $b[24] ^ $b[26] ^ $b[27] ^ $b[29] ^ $b[30];
    $p[2] = $b[1]  ^ $b[2]  ^ $b[3]  ^ $b[7]  ^ $b[9]  ^ $b[10] ^ $b[11] ^ $b[15] ^ $b[17] ^ $b[18] ^ $b[19] ^ $b[23] ^ $b[25] ^ $b[26] ^ $b[27] ^ $b[31];
    $p[3] = $b[4]  ^ $b[5]  ^ $b[6]  ^ $b[7]  ^ $b[12] ^ $b[13] ^ $b[14] ^ $b[15] ^ $b[20] ^ $b[21] ^ $b[22] ^ $b[23] ^ $b[28] ^ $b[29] ^ $b[30] ^ $b[31];
    $p[4] = $b[8]  ^ $b[9]  ^ $b[10] ^ $b[11] ^ $b[12] ^ $b[13] ^ $b[14] ^ $b[15] ^ $b[24] ^ $b[25] ^ $b[26] ^ $b[27] ^ $b[28] ^ $b[29] ^ $b[30] ^ $b[31];
    $p[5] = $b[16] ^ $b[17] ^ $b[18] ^ $b[19] ^ $b[20] ^ $b[21] ^ $b[22] ^ $b[23] ^ $b[24] ^ $b[25] ^ $b[26] ^ $b[27] ^ $b[28] ^ $b[29] ^ $b[30] ^ $b[31];
    $p[6] = $b[0]  ^ $b[1]  ^ $b[2]  ^ $b[3]  ^ $b[4]  ^ $b[5]  ^ $b[6]  ^ $b[7]  ^ $b[8]  ^ $b[9]  ^ $b[10] ^ $b[11] ^ $b[12] ^ $b[13] ^ $b[14] ^ $b[15] ^ $b[16] ^ $b[17] ^ $b[18] ^ $b[19] ^ $b[20] ^ $b[21] ^ $b[22] ^ $b[23] ^ $b[24] ^ $b[25] ^ $b[26] ^ $b[27] ^ $b[28] ^ $b[29] ^ $b[30] ^ $b[31] ^ $p[0] ^ $p[1] ^ $p[2] ^ $p[3] ^ $p[4] ^ $p[5];

    my $ECC = ($p[6] << 6)
            | ($p[5] << 5)
            | ($p[4] << 4)
            | ($p[3] << 3)
            | ($p[2] << 2)
            | ($p[1] << 1)
            |  $p[0];

    $ECC = ~$ECC & 0x7F;

    return $ECC;
}


#****************************************************************************************
#
# Unit Name:    Checksum
#
# Parameters:   $record
#
# Return Value: The calculated checksum.
#
# Description:  Given a string of bytes that represent an Intel hex file record,
#               return the checksum as calculated per the Intel hex file standard.
#
#*****************************************************************************************
sub Checksum
{
   my ($record) = @_;

   my $index = 1;  # Skip the initial colon
   my $checksum = 0;
   my $record_length;

   $record_length = length($record);
   while ($index < $record_length)
   {
      $checksum += hex(substr($record, $index, 2));
      $index += 2;
   }

   return ((0x100 - ($checksum % 256)) % 256);
}


#****************************************************************************************
#
# Unit Name:    Calculate_CRC
#
# Parameters:   $arrayRef: A reference to an array which contains the data values from
#                          an Intel hex file, indexed by address.
#               $min_addr: The minimum address encountered in the Intel hex file. This
#                          value has been subtracted from all of the hex file addresses
#                          to arrive at the array indeces (i.e., the hex file addresses
#                          might start at, say, 0x10000000, but the values in the array
#                          start at index 0).
#               $max_addr: The maximum address encountered in the Intel hex file.
#
# Return Value: The calculated CRC.
#
# Description:  Given an array of hex bytes that represent the data content of an Intel
#               hex file, calculate and return the CCITT-16 CRC for the data.
#
#*****************************************************************************************
sub Calculate_CRC
{
   my ($arrayRef, $min_addr, $max_addr) = @_;

   my $crc = 0xFFFF;  # The CRC must be seeded with 0xFFFF
   for (my $index=0; $index <= ($max_addr - $min_addr); $index++) {
      my $tmp = $crc ^ (hex(@{$arrayRef}[$index]) & 0xFF);
      $crc = (((($tmp<<12) & 0xf000) | (($tmp>>4) & 0x000f)) ^
                (((($tmp<< 8) & 0xff00) | (($tmp>>8) & 0x00ff)) ^
                ((( $tmp<< 7) & 0x0780) ^
                ((( $tmp    ) & 0x000f) ^ (($tmp<<3) & 0x07f8)))));
   }
   return ($crc);
}


#****************************************************************************************
#
# Unit Name:    Parse_Intel_Hex_File
#
# Parameters:   $hexFileHandle: The handle of the Intel hex file to be processed.
#               $arrayRef: A reference to the array which will be populated with the hex
#                          file contents.
#               $start_addr: The minimum address of the memory space in the hex file. It
#                            is assumed that the code starts at this address (i.e., there
#                            are no unused addresses at the start of the memory space).
#               $mem_size: The sizeof the memory space in the hex file.
#
# Return Value: The maximum code address encountered in the hex file.
#
# Description:  Parse an intel hex file using the given filehandle, stripping off all of
#               the Intel file format data (ID, address, byte count, checksum) and placing
#               the data into an array indexed by address. This subroutine will also strip
#               out any unnecessary records (i.e., records of type 3 or 5, which contain
#               80x86-specific info).
#
#               There's a limit to how large an array can be accommodated by this unit. So,
#               when we're dealing with an address range of 0x10000000-0x1007FFFF as in the
#               Galaxy flash (or 0x01000000-0x0101FFFF for ROM), we have to subtract the
#               start address from the memory_array index to get the index (and thus the
#               array size) into an acceptable range. That's why it's necessary to provide
#               the starting address as an argument, and why you'll see the array referenced
#               like "@{$arrayRef}[$addr - $start_addr]".
#
#               See file header for details of Intel hex file format.
#
#*****************************************************************************************
sub Parse_Intel_Hex_File
{
   my ($hexFileHandle, $arrayRef, $start_addr, $mem_size) = @_;

   my $line_count;
   my $max_code_addr  = 0;
   my $max_code_addr_line_count;
   my $base_addr;
   my $addr_struct;

   while ( my $input_line = <$hexFileHandle> )
   {
      if ($input_line =~ /^\:(\w\w)(\w\w\w\w)(\w\w)(\w*)(\w\w)$/)
      {
         my $length   = hex($1);
         my $addr     = hex($2) + $base_addr;
         my $type     = $3;
         my $data     = $4;
         my $checksum = hex($5);

         # Check for invalid checksum for the record
         my $calculated_checksum = &Checksum(substr($input_line, 0, length($input_line)-3));
         if ($checksum != $calculated_checksum)
         {
            Log(sprintf "ERROR: invalid checksum encountered (Line $line_count)\n");
            Log(sprintf "       $input_line");
            Log(sprintf "       Expected: %02X     Calculated: %02X\n", $checksum, $calculated_checksum);
            exit();
         }

         # Check for invalid record type
         if (($type < 0) || ($type > 5))
         {
            Log(sprintf "ERROR: out-of-range record type (%d) encountered (Line $line_count, starting address 0x%05X)\n", $type, $addr);
            exit();
         }

         # Normal data line.
         if ($type == 0)
         {
            # Check for out-of-range length field
            if ($length > $DATA_LENGTH)
            {
               Log(sprintf "ERROR: invalid record length ($length) encountered - should be <= $DATA_LENGTH (Line $line_count, starting address 0x%05X)\n", $addr);
               exit();
            }
            if (length( $input_line) != (12 + ($length * 2))) {
               Log(sprintf "ERROR: invalid data record encountered (Line $line_count)\n");
               exit();
            }


            # Check for out-of-range address
            if (($addr < $start_addr) || ($addr >= ($start_addr + $mem_size))) {
               Log (sprintf "ERROR Encountered out-of-range address (0x%08X) at line $line_count.\n", $addr);
               exit();
            }

            for (my $i=0; $i<$length; $i++)
            {
               @{$arrayRef}[$addr - $start_addr] = substr($data, $i*2, 2);

               # Determine max address.
               if (($addr + $DATA_LENGTH - 1) > $max_code_addr)
               {
                  $max_code_addr = $addr;
                  $max_code_addr_line_count = $line_count;
               }
               $addr++;
            }
         }

         # Extended Segment Address line. These were used for the legacy platform
         # to provide an address range up to 1M, but aren't used on the Galaxy platform
         # (Galaxy will use the "Start Linear Address" record instead, since it can
         # accommodate an address range up to 4G).
         elsif ($type == 2)
         {
            Log(sprintf "WARNING: Extended Segment Address record ignored (Line $line_count)\n");
         }

         # Start Segment Address line. This is an 80x86-specific record, so Galaxy won't
         # use it. The fromelf tool seems to like generating these records, though, so
         # we'll see the occasional warning.
         elsif ($type == 3)
         {
            Log(sprintf "WARNING: Start Segment Address record ignored (Line $line_count)\n");
         }

         # Extended Linear Address line.
         elsif ($type == 4)
         {
            # Check for out-of-range length field
            if ($length != 2)
            {
               Log(sprintf "ERROR: invalid Extended Segment Address record length (%d) encountered - should be 2 (Line $line_count, starting address 0x%05X)\n", $length, $addr);
               exit();
            }
            if (length( $input_line) != 16) {
               Log(sprintf "ERROR: invalid data record encountered (Line $line_count)\n");
               exit();
            }

            $base_addr= hex($data) * 0x10000;
         }

         # Start Linear Address line. This is an 80x86-specific record, so Galaxy won't
         # use it. The fromelf tool seems to like generating these records, though, so
         # we'll see the occasional warning.
         elsif ($type == 5)
         {
            #Log(sprintf "WARNING: Start Linear Address record ignored (Line $line_count)\n");
         }
      }
      $line_count++;
   }# End while 

   return ($max_code_addr);
}  # End sub Parse_Intel_Hex_File


#****************************************************************************************
#
# Unit Name:    Generate_Hex_File
#
# Parameters:   $hexFileHandle: The handle of the Intel Hex file to be generated.
#               $arrayRef: A reference to the array holding the hex file contents.
#               $min_addr: The starting address of the code in the hex file. This is
#                          subtracted from the hex file addresses to determine the array
#                          index for the data. This accommodates hex files with large
#                          starting addresses.
#               $max_addr: The ending address of the code in the hex file. Note that
#                          this is the end of the FW code, not the end of the address
#                          space.
#
# Return Value: None
#
# Description:  Given a memory array containing code values, write the values out to the
#               given file handle in Intel hex file format. The address range in the hex
#               file will be determined by the given minimum and maximum addresses (i.e.,
#               the memory array is indexed starting at 0, and the minimum address will
#               be added to the array index to provide the hex file address). Only the
#               used addresses will be output (i.e., unused addresses will not be filled).
#
#               See file header for details of Intel hex file format.
#
#*****************************************************************************************
sub Generate_Hex_File
{
   my ($hexFileHandle, $arrayRef, $min_addr, $max_addr) = @_;
   my $output_length;
   my $initial_ELA;

   # Intel hex file records have an address range of 64K. If the starting address is >64K,
   # an Extended Linear Address record must be inserted at the beginning of the output
   # file.
   if (($min_addr > 0x10000)) {
      $initial_ELA = 1;
   }

   # The output_length determines how many data bytes will be output in a single record.
   # Normally output_length will be the same as DATA_LENGTH (which is hardcoded as either
   # 16 or 32). The two possible exceptions are the first data record in the file (which
   # is handled here) and the last data record in the file (which is handled in the for
   # loop below).
   $output_length = $DATA_LENGTH - ($min_addr % $DATA_LENGTH); 

   # Step through the memory array to generate the hex output file, one line at a time.
   for (my $addr=$min_addr; $addr<=$max_addr; $addr+=$output_length)
   {
      my $record;

      # Intel hex file records have an address range of 64K. If the address crosses a 64K
      # boundary, an Extended Linear Address record must be inserted in the output file.
      # Since we're outputting the records in ascending address order, we can simply check
      # for address mod 64K to detect the boundary crossings.
      if (($addr % 0x10000 == 0) || $initial_ELA) {
         $record = sprintf ":02000004%04X", $addr / 0x10000;
         printf $hexFileHandle "$record%02X\n", &Checksum($record);
         $initial_ELA = 0;
      }

      # Insert a record "header" (colon, byte count, address, record type) when starting
      # a new line. The record type is 00 for a Data record.
      if ($addr % $DATA_LENGTH == 0) {
         if (($max_addr - $addr) >= $DATA_LENGTH) {  # A "normal" data line
            $output_length = $DATA_LENGTH;
         }
         else {  # Handle a partial line at the end of the file
            $output_length = $max_addr - $addr + 1; 
         }
         $record = sprintf ":%02X%04X00", $output_length, $addr % 0x10000;  
      }
      # Output the data bytes for one line. Since this script is intended to be run with
      # an input file in which the entire memory range is filled, any undefined memory 
      # value that is encountered will result in an error message and termination of the
      # script.
      for (my $offset=0; $offset<$output_length; $offset++) {
         if (!defined(@{$arrayRef}[$addr+$offset-$min_addr]))
         {
            Log(sprintf "ERROR: undefined value at address %08X\n", $addr+$offset);
            exit();
         }
         else
         {
            $record .= @{$arrayRef}[$addr+$offset-$min_addr];  # Append a byte to the record.
         }
      }

      # Ouput the record, appending the checksum at the end of the record.
      printf $hexFileHandle "$record%02X\n", &Checksum($record);  
   }

   #Output the EOF record
   printf $hexFileHandle ":00000001FF\n";
}  # End sub Generate_Hex_File

